#include "sfx.h"

//#include <stdio.h>
//#include <string.h> /* strlen() */
#include <windows.h>
#include <objbase.h>
#include <MMReg.h>
#include <math.h> // TEMP

#define DIRECTSOUND_VERSION 0x0300
#include <dsound.h>
// #ifdef PAWIN_USE_WDMKS_DEVICE_INFO
// #include <dsconf.h>
// #endif /* PAWIN_USE_WDMKS_DEVICE_INFO */

#include "sfx_win32_ds_dynlink.h"
#include "tl/vector.h"
#include "tl/memory.h"

#if TL_MSVCPP
#pragma comment( lib, "winmm.lib" )
#endif

typedef struct ds_device
{
	sfx_device base;

	double sample_rates[3];

	GUID guid;
	char guid_is_null;

} ds_device;

typedef struct ds_devices
{
	tl_vector vec[2];
} ds_devices;

typedef struct sfx_win32
{
	COMMON_SFX_BASE()

	ds_devices devices;
	double seconds_per_tick;
	char com_was_initialized;
	char timer_initialized;
	char use_perf_counter;
	
} sfx_win32;

#define CHECK(r) do { if(r) goto fail; } while(0)
#define CHECKB(r) do { if(!(r)) goto fail; } while(0)
#define CHECKR(r) do { err = (r); if(err) goto fail; } while(0)

#include "sfx_common.h"

static void ds_device_deinit(ds_device* self)
{
	sfx_device_deinit(&self->base);
}

#define OUTPUT_DEVICES (0)
#define INPUT_DEVICES (1)

static void ds_device_init(ds_device* self, LPGUID lpGUID, LPCTSTR lpszDesc)
{
	sfx_device_init(&self->base, lpszDesc);
	self->guid_is_null = 1;
	// TODO: Check strdup return
	if(NULL != lpGUID)
	{
		self->guid = *lpGUID;
		self->guid_is_null = 0;
	}
}

static void ds_devices_init(ds_devices* self)
{
	int i;
	for(i = 0; i < 2; ++i)
		tl_vector_new_empty(self->vec[i]);
}


static void ds_devices_deinit(ds_devices* self)
{
	int i;
	for(i = 0; i < 2; ++i)
	{
		tl_vector_foreach(self->vec[i], ds_device, g, {
			ds_device_deinit(g);
		});

		tl_vector_free(self->vec[i]);
	}
}

static int CALLBACK collect_guids(void* lpGUID,
    char const* lpszDesc,
    char const* lpszDrvName,
    void* lpContext)
{
	ds_device dev;
	tl_vector* vec = (tl_vector*)lpContext;

	ds_device_init(&dev, lpGUID, lpszDesc);
	tl_pushback(*vec, ds_device, dev);

	return 1;
}

static int ds_gather_output_device_info(ds_device* dev)
{
	HRESULT err;
	GUID* guid = dev->guid_is_null ? NULL : &dev->guid;
	IDirectSound* directSound = NULL;
	
	// TODO?
	// if (IsEqualGUID (&IID_IRolandVSCEmulated1,lpGUID) ||
    // IsEqualGUID (&IID_IRolandVSCEmulated2,lpGUID) )

	CHECKR(sfx_ds.DirectSoundCreate(guid, &directSound, NULL));
	
	{
		DSCAPS caps;
		memset(&caps, 0, sizeof(caps));
        caps.dwSize = sizeof(caps);
		CHECKR(IDirectSound_GetCaps(directSound, &caps));

		if(caps.dwFlags & DSCAPS_PRIMARYSTEREO)
			dev->base.max_out_channels = 2; // TODO: More channels may be supported
		else
			dev->base.max_out_channels = 1;

		if(caps.dwFlags & DSCAPS_CONTINUOUSRATE)
		{
			dev->base.default_sample_rate = caps.dwMaxSecondarySampleRate;

			// TODO: Normalize to a default sample rate
		}
		else if(caps.dwMinSecondarySampleRate == caps.dwMaxSecondarySampleRate)
        {
            if(caps.dwMinSecondarySampleRate == 0)
				dev->base.default_sample_rate = 44100.0;
			else
				dev->base.default_sample_rate = caps.dwMaxSecondarySampleRate;
		}
		else if((caps.dwMinSecondarySampleRate < 1000) && (caps.dwMaxSecondarySampleRate > 50000))
        {
            dev->base.default_sample_rate = 44100.0;
		}
		else
		{
			dev->base.default_sample_rate = caps.dwMaxSecondarySampleRate;
		}
	}

	return 0;

fail:
	if(directSound) IDirectSound_Release(directSound);
	return err;
}

sfx* sfx_create(void)
{
	HRESULT hr;
	sfx_win32* self = calloc(1, sizeof(sfx_win32));
	// TODO: Check pointers from malloc. Applies to all calls!
	// NOTE: Do not jump to fail if malloc fails here!

	hr = CoInitialize(NULL);

	CHECKB(!FAILED(hr) || hr == RPC_E_CHANGED_MODE);

    if(hr != RPC_E_CHANGED_MODE)
        self->com_was_initialized = 1;

	sfx_ds_init_ds_entrypoints();

	ds_devices_init(&self->devices);

	sfx_ds.DirectSoundEnumerate((LPDSENUMCALLBACK)collect_guids, (void*)&self->devices.vec[0]);

	// Gather info and filter out devices we don't want
	tl_vector_filtereach(self->devices.vec[0], ds_device, dev, {
		if(ds_gather_output_device_info(dev))
			remove = 1;
	});

	return (sfx*)self;

fail:
	if(self->com_was_initialized)
        CoUninitialize();
	free(self);
	return NULL;
}

void sfx_destroy(sfx* self_)
{
	sfx_win32* self = (sfx_win32*)self_;

	ds_devices_deinit(&self->devices);
	free(self);
}

typedef struct ds_stream
{
	sfx_stream base;

	IDirectSound*        directSound;
    IDirectSoundBuffer*  directSoundOutputBuffer;
	int framesPerDsBuffer;
	int outputBufferSizeBytes;
	DWORD outputBufferWriteOffsetBytes;     /* last write position */
	MMRESULT timerId;

	LARGE_INTEGER  perfCounterTicksPerBuffer; /* counter ticks it should take to play a full buffer */
    LARGE_INTEGER  previousPlayTime;
    UINT           previousPlayCursor;
	double         dsw_framesWritten;
    double         framesPlayed;
	int            bytesPerOutputFrame;
} ds_stream;

static GUID dataFormatSubtypePcm = 
	{ (USHORT)(WAVE_FORMAT_PCM), 0x0000, 0x0010, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71 };

static void waveformatex_init(WAVEFORMATEXTENSIBLE* waveFormat,
	int numChannels, double sampleRate,
	unsigned long channelMask,
	int extensible)
{
	WAVEFORMATEX *waveFormatEx = (WAVEFORMATEX*)waveFormat;
    int bytesPerSample = 2;
	unsigned long bytesPerFrame = numChannels * bytesPerSample;

	waveFormatEx->wFormatTag = extensible ? WAVE_FORMAT_EXTENSIBLE : WAVE_FORMAT_PCM;
	waveFormatEx->nChannels = (WORD)numChannels;
	waveFormatEx->nSamplesPerSec = (DWORD)sampleRate;
	waveFormatEx->nAvgBytesPerSec = waveFormatEx->nSamplesPerSec * bytesPerFrame;
	waveFormatEx->nBlockAlign = (WORD)bytesPerFrame;
	waveFormatEx->wBitsPerSample = bytesPerSample * 8;
	waveFormatEx->cbSize = extensible ? 22 : 0;

	if(extensible)
	{
		waveFormat->Samples.wValidBitsPerSample = waveFormatEx->wBitsPerSample;
		waveFormat->dwChannelMask = channelMask;
		waveFormat->SubFormat = dataFormatSubtypePcm;
	}
}

static unsigned long default_channel_mask(int numChannels)
{
	switch( numChannels ){
		case 1: return PAWIN_SPEAKER_MONO;
		case 2: return PAWIN_SPEAKER_STEREO; 
		case 3: return PAWIN_SPEAKER_FRONT_LEFT | PAWIN_SPEAKER_FRONT_CENTER | PAWIN_SPEAKER_FRONT_RIGHT;
		case 4: return PAWIN_SPEAKER_QUAD;
		case 5: return PAWIN_SPEAKER_QUAD | PAWIN_SPEAKER_FRONT_CENTER;
		case 6: return PAWIN_SPEAKER_5POINT1; 
        /* case 7: */
		case 8: return PAWIN_SPEAKER_7POINT1;
	}

	return PAWIN_SPEAKER_DIRECTOUT;
}

#define PA_LATENCY_ENV_NAME  ("PA_MIN_LATENCY_MSEC")
#define PA_ENV_BUF_SIZE  (32)

static int ds_get_min_latency_frames(double sampleRate)
{
#if 0
    char      envbuf[PA_ENV_BUF_SIZE];
    DWORD     hresult;
    int       minLatencyMsec = 0;

    /* Let user determine minimal latency by setting environment variable. */
    hresult = GetEnvironmentVariable( PA_LATENCY_ENV_NAME, envbuf, PA_ENV_BUF_SIZE );
    if( (hresult > 0) && (hresult < PA_ENV_BUF_SIZE) )
    {
        minLatencyMsec = atoi( envbuf );
    }
    else
#endif

	int minLatencyMsec = 280; // TODO: Decide based on OS version
    return (int)(minLatencyMsec * sampleRate * 0.001);
}

sfx_stream* sfx_open(sfx* self_, void (*fill)(sfx_stream* str, uint32_t start, uint32_t frames))
{
	HRESULT err;
	IDirectSoundBuffer* primaryBuffer = NULL;
	IDirectSoundBuffer* directSoundOutputBuffer = NULL;
	sfx_win32* self = (sfx_win32*)self_;
	ds_stream* str = calloc(1, sizeof(ds_stream));
	ds_device* dev = tl_vector_idx(self->devices.vec[OUTPUT_DEVICES], ds_device, 0); // TODO: Select default device

	GUID* guid = dev->guid_is_null ? NULL : &dev->guid;
	IDirectSound* directSound = NULL;

	CHECKR(sfx_ds.DirectSoundCreate(guid, &directSound, NULL));
	str->directSound = directSound;
	
	{
		// Buffer
		DSBUFFERDESC primaryDesc, secondaryDesc;
		WAVEFORMATEXTENSIBLE waveFormat;		
		HWND wnd = GetDesktopWindow();
		unsigned int bytesPerSample = 2;
		unsigned int nChannels = 2;
		unsigned int nFrameRate = 44100;
		int framesPerDsBuffer = ds_get_min_latency_frames(nFrameRate);
		unsigned int bytesPerOutputFrame = nChannels * bytesPerSample;
		int bytesPerDirectSoundBuffer = framesPerDsBuffer * bytesPerOutputFrame;
		unsigned long channelMask = default_channel_mask(nChannels);
		unsigned char* dsBuffData;
		unsigned int bytesPerBuffer = bytesPerDirectSoundBuffer;
		unsigned int outputBufferSizeBytes = bytesPerBuffer;
		DWORD dwDataLen;
		DWORD playCursor;
		LARGE_INTEGER counterFrequency;

		str->base.owner = self_;
		str->base.sample_rate = nFrameRate;
		str->base.stream_pos = 0;
		str->base.buffer = malloc(outputBufferSizeBytes); // TODO: Make sure it is freed
		str->base.fill = fill;
		str->framesPerDsBuffer = framesPerDsBuffer;
		str->outputBufferSizeBytes = outputBufferSizeBytes;
		str->bytesPerOutputFrame = bytesPerOutputFrame;

		CHECKR(IDirectSound_SetCooperativeLevel(directSound, wnd, DSSCL_EXCLUSIVE));

		memset(&primaryDesc, 0, sizeof(DSBUFFERDESC));
		primaryDesc.dwSize        = sizeof(DSBUFFERDESC);
		primaryDesc.dwFlags       = DSBCAPS_PRIMARYBUFFER; // all panning, mixing, etc done by synth
		primaryDesc.dwBufferBytes = 0;
		primaryDesc.lpwfxFormat   = NULL;

		// Create the buffer
		CHECKR(IDirectSound_CreateSoundBuffer(directSound, &primaryDesc, &primaryBuffer, NULL));

		waveformatex_init(&waveFormat, nChannels, nFrameRate, channelMask, 1);

		if(IDirectSoundBuffer_SetFormat(primaryBuffer, (WAVEFORMATEX*)&waveFormat) != DS_OK)
		{
			waveformatex_init(&waveFormat, nChannels, nFrameRate, channelMask, 0);
			CHECKR(IDirectSoundBuffer_SetFormat(primaryBuffer, (WAVEFORMATEX*)&waveFormat));
		}

		// ----------------------------------------------------------------------
		// Setup the secondary buffer description
		memset(&secondaryDesc, 0, sizeof(DSBUFFERDESC));
		secondaryDesc.dwSize = sizeof(DSBUFFERDESC);
		secondaryDesc.dwFlags =  DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2;
		secondaryDesc.dwBufferBytes = bytesPerDirectSoundBuffer;
		secondaryDesc.lpwfxFormat = (WAVEFORMATEX*)&waveFormat;

		// Create the secondary buffer
		CHECKR(IDirectSound_CreateSoundBuffer(directSound, &secondaryDesc, &directSoundOutputBuffer, NULL));
		str->directSoundOutputBuffer = directSoundOutputBuffer;

		CHECKR(IDirectSoundBuffer_Lock(directSoundOutputBuffer, 0, outputBufferSizeBytes, (void**)&dsBuffData, &dwDataLen, NULL, 0, 0));

		memset(dsBuffData, 0, dwDataLen);

		CHECKR(IDirectSoundBuffer_Unlock(directSoundOutputBuffer, dsBuffData, dwDataLen, NULL, 0));

		str->perfCounterTicksPerBuffer.QuadPart = 0;
		if(QueryPerformanceFrequency(&counterFrequency))
		{
			int framesInBuffer = bytesPerBuffer / (nChannels * bytesPerSample);
			str->perfCounterTicksPerBuffer.QuadPart = (counterFrequency.QuadPart * framesInBuffer) / nFrameRate;
		}
		// Let DSound set the starting write position because if we set it to zero, it looks like the
		// buffer is full to begin with. This causes a long pause before sound starts when using large buffers.
		CHECKR(IDirectSoundBuffer_GetCurrentPosition(directSoundOutputBuffer,
				&playCursor, &str->outputBufferWriteOffsetBytes));
		str->dsw_framesWritten = str->outputBufferWriteOffsetBytes / bytesPerOutputFrame;

		
	}

	return &str->base;

fail:
	sfx_stream_destroy(&str->base);
	return NULL;
}

static HRESULT query_output_space(ds_stream* str, long* bytesEmpty)
{
    HRESULT err = DS_OK;
    DWORD playCursor;
    DWORD writeCursor;
    long  numBytesEmpty;
    long  playWriteGap;
    // Query to see how much room is in buffer.

    CHECKR(IDirectSoundBuffer_GetCurrentPosition(str->directSoundOutputBuffer,
            &playCursor, &writeCursor));
    // Determine size of gap between playIndex and WriteIndex that we cannot write into.
    playWriteGap = writeCursor - playCursor;
    if(playWriteGap < 0)
		playWriteGap += str->outputBufferSizeBytes; // unwrap
    /* DirectSound doesn't have a large enough playCursor so we cannot detect wrap-around. */
    /* Attempt to detect playCursor wrap-around and correct it. */

    if(str->base.outputIsRunning && (str->perfCounterTicksPerBuffer.QuadPart != 0))
    {
        /* How much time has elapsed since last check. */
        LARGE_INTEGER   currentTime;
        LARGE_INTEGER   elapsedTime;
        long            bytesPlayed;
        long            bytesExpected;
        long            buffersWrapped;
        QueryPerformanceCounter(&currentTime);
        elapsedTime.QuadPart = currentTime.QuadPart - str->previousPlayTime.QuadPart;
        str->previousPlayTime = currentTime;
        /* How many bytes does DirectSound say have been played. */
        bytesPlayed = playCursor - str->previousPlayCursor;
        if(bytesPlayed < 0) bytesPlayed += str->outputBufferSizeBytes; // unwrap
        str->previousPlayCursor = playCursor;
        /* Calculate how many bytes we would have expected to been played by now. */
        bytesExpected = (long) ((elapsedTime.QuadPart * str->outputBufferSizeBytes) / str->perfCounterTicksPerBuffer.QuadPart);
        buffersWrapped = (bytesExpected - bytesPlayed) / str->outputBufferSizeBytes;
        if(buffersWrapped > 0)
        {
            playCursor += (buffersWrapped * str->outputBufferSizeBytes);
            bytesPlayed += (buffersWrapped * str->outputBufferSizeBytes);
        }
        /* Maintain frame output cursor. */
        str->framesPlayed += (bytesPlayed / str->bytesPerOutputFrame);
    }

    numBytesEmpty = playCursor - str->outputBufferWriteOffsetBytes;
    if(numBytesEmpty < 0)
		numBytesEmpty += str->outputBufferSizeBytes; // unwrap offset
    /* Have we underflowed? */
    if(numBytesEmpty > (str->outputBufferSizeBytes - playWriteGap))
    {
        if(str->base.outputIsRunning)
            str->base.outputUnderflowCount += 1;
        str->outputBufferWriteOffsetBytes = writeCursor;
        numBytesEmpty = str->outputBufferSizeBytes - playWriteGap;
    }
    *bytesEmpty = numBytesEmpty;
fail:
    return err;
}

static void CALLBACK process_stream(UINT uID, UINT uMsg, DWORD_PTR dwUser, DWORD dw1, DWORD dw2)
{
	HRESULT err;
	long bytesEmpty, framesToXfer, numOutFramesReady, bytesToXfer;
	long bytesProcessed, numFrames;
	double outputBufferDacTime;
	double currentTime;
	double outputLatency = 0.0;
	
	LPBYTE lpOutBuf1 = NULL;
    LPBYTE lpOutBuf2 = NULL;
    DWORD  dwOutSize1 = 0;
    DWORD  dwOutSize2 = 0;
	ds_stream* str = (ds_stream*)dwUser;

	if(!str->base.outputIsRunning)
		return;

	(void)uID;
    (void)uMsg;
    (void)dw1;
    (void)dw2;

	query_output_space(str, &bytesEmpty);

    framesToXfer = numOutFramesReady = bytesEmpty / str->bytesPerOutputFrame;
	bytesToXfer = framesToXfer * str->bytesPerOutputFrame;

    CHECKR(IDirectSoundBuffer_Lock(str->directSoundOutputBuffer,
        str->outputBufferWriteOffsetBytes, bytesToXfer,
        (void**)&lpOutBuf1, &dwOutSize1,
        (void**)&lpOutBuf2, &dwOutSize2, 0));

	numFrames = (dwOutSize1 + dwOutSize2) / str->bytesPerOutputFrame;
	currentTime = sfx_get_time(str->base.owner);
    outputBufferDacTime = currentTime + outputLatency;

	// TODO: Make sure underflows are counted even if no data are produced for them.
	str->base.stream_pos += numFrames;
	TL_WRITE_SYNC();

	if(str->base.fill)
	{
		str->base.fill(&str->base, str->base.stream_pos, numFrames);
		memcpy(lpOutBuf1, (char*)str->base.buffer, dwOutSize1);
		memcpy(lpOutBuf2, (char*)str->base.buffer + dwOutSize1, dwOutSize2);
	}
	else
	{
		memset(lpOutBuf1, 0, dwOutSize1);
		memset(lpOutBuf2, 0, dwOutSize2);
	}

	bytesProcessed = numFrames * str->bytesPerOutputFrame;
    str->outputBufferWriteOffsetBytes = (str->outputBufferWriteOffsetBytes + bytesProcessed) % str->outputBufferSizeBytes;
    IDirectSoundBuffer_Unlock(str->directSoundOutputBuffer, lpOutBuf1, dwOutSize1, lpOutBuf2, dwOutSize2);
    str->dsw_framesWritten += numFrames;

	str->base.successfulCalls += 1;

fail: ;
	// TODO: Remember error?
	
}

int sfx_start(sfx_stream* str_)
{
	HRESULT err;
	ds_stream* str = (ds_stream*)str_;
	if(str->base.outputIsRunning)
		return 0; // Already running

	QueryPerformanceCounter(&str->previousPlayTime);
    str->previousPlayCursor = 0;
    str->framesPlayed = 0.0;
    CHECKR(IDirectSoundBuffer_SetCurrentPosition(str->directSoundOutputBuffer, 0));
	
	{
        int resolution;
        int framesPerWakeup = str->framesPerDsBuffer / 4;
        int msecPerWakeup = 1000 * framesPerWakeup / (int)str->base.sample_rate;
        if(msecPerWakeup < 10) msecPerWakeup = 10;
        else if(msecPerWakeup > 100) msecPerWakeup = 100;
        resolution = msecPerWakeup/4;
        str->timerId = timeSetEvent(msecPerWakeup, resolution, (LPTIMECALLBACK)process_stream,
                                             (DWORD_PTR)str, TIME_PERIODIC | TIME_KILL_SYNCHRONOUS);
    }

	CHECKR(IDirectSoundBuffer_Play(str->directSoundOutputBuffer, 0, 0, DSBPLAY_LOOPING));
    str->base.outputIsRunning = 1;

	return 0;

fail:
	return -1;
}

int sfx_stop(sfx_stream* str_)
{
	HRESULT err;
	ds_stream* str = (ds_stream*)str_;

	if(str->base.outputIsRunning)
	{
		CHECKR(IDirectSoundBuffer_Stop(str->directSoundOutputBuffer));

		str->base.outputIsRunning = 0;
		TL_WRITE_SYNC(); // Make sure outputIsRunning is written before event is killed.
		timeKillEvent(str->timerId);

		// TODO: Wait until processing is actually done. If we kill the timer while
		// an event is being processed we must wait until it's done to be safe.
	}

	return 0;
fail:
	return -1;
}

void sfx_stream_destroy(sfx_stream* str_)
{
	ds_stream* str = (ds_stream*)str_;

	sfx_stop(str_);

	free(str->base.buffer);
	if(str->directSoundOutputBuffer) IDirectSoundBuffer_Release(str->directSoundOutputBuffer);
	if(str->directSound) IDirectSound_Release(str->directSound);
	free(str_);
}

uint32_t sfx_stream_get_pos(sfx_stream* str_)
{
	ds_stream* str = (ds_stream*)str_;
	TL_READ_SYNC();
	return str_->stream_pos;
}

double sfx_get_time(sfx* self_)
{
	LARGE_INTEGER time;
	sfx_win32* self = (sfx_win32*)self_;

	if(!self->timer_initialized)
	{
		LARGE_INTEGER ticksPerSecond;

		if(QueryPerformanceFrequency(&ticksPerSecond) != 0)
		{
			self->use_perf_counter = 1;
			self->seconds_per_tick = 1.0 / (double)ticksPerSecond.QuadPart;
		}
		else
		{
			self->use_perf_counter = 0;
		}
	}

    if(self->use_perf_counter)
    {
		// TODO: If deviation is detected (relative to timeGetTime())
		// revert to using timeGetTime().

        /* FIXME:
            according to this knowledge-base article, QueryPerformanceCounter
            can skip forward by seconds!
            http://support.microsoft.com/default.aspx?scid=KB;EN-US;Q274323&

            it may be better to use the rtdsc instruction using inline asm,
            however then a method is needed to calculate a ticks/seconds ratio.
        */
        QueryPerformanceCounter(&time);
        return time.QuadPart * self->seconds_per_tick;
    }
    else
    {	
        return timeGetTime() * 0.001;
    }
}